package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.mapper.MediaProcessMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @description TODO
 * @author Mr.M
 * @date 2022/9/10 8:58
 * @version 1.0
 */
 @Service
 @Slf4j
public class MediaFileServiceImpl implements MediaFileService {

      @Autowired
      MediaFilesMapper mediaFilesMapper;
      @Autowired
      MinioClient minioClient;
      //存储普通文件
      @Value("${minio.bucket.files}")
      private String bucket_mediaFiles;
     //存储视频文件
      @Value("${minio.bucket.videofiles}")
      private String bucket_video;

      @Autowired
      MediaFileService currentProxy;
      @Autowired
      MediaProcessMapper mediaProcessMapper;



    @Override
     public PageResult<MediaFiles> queryMediaFiles(Long companyId,PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

            //构建查询条件对象
            LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
            //模糊查询媒资文件名称
            queryWrapper.like(StringUtils.isNotEmpty(queryMediaParamsDto.getFilename()),MediaFiles::getFilename,queryMediaParamsDto.getFilename());
            //根据媒资类型查询
            queryWrapper.eq(StringUtils.isNotEmpty(queryMediaParamsDto.getFileType()),MediaFiles::getFileType,queryMediaParamsDto.getFileType());
            //分页对象
            Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
            //构建分页查询条件
            // 查询数据内容获得结果
            Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
            // 获取数据列表
            List<MediaFiles> list = pageResult.getRecords();
            // 获取数据总数
            long total = pageResult.getTotal();
            // 构建结果集
            PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
            return mediaListResult;

     }

        //获取文件默认存储目录路径 年/月/日
        private String getDefaultFolderPath() {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            String folder = sdf.format(new Date()).replace("-", "/")+"/";
            return folder;
        }

        //获取文件的md5
        private String getFileMd5(File file) {
            try (FileInputStream fileInputStream = new FileInputStream(file)) {
                String fileMd5 = DigestUtils.md5Hex(fileInputStream);
                fileInputStream.close();
                return fileMd5;
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        }

        //根据拓展名获取mimeType
        private String getMimeType(String extension){
            if(extension==null)
                extension = "";
            //根据扩展名取出mimeType
            ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
            //通用mimeType，字节流
            String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;
            if(extensionMatch!=null){
                mimeType = extensionMatch.getMimeType();
            }
            return mimeType;
        }
        /**
         * @description 将文件写入minIO
         * @param localFilePath  文件地址
         * @param bucket  桶
         * @param objectName 对象名称
         * @return void
         * @author Mr.M
         * @date 2022/10/12 21:22
         */
        public boolean addMediaFilesToMinIO(String localFilePath,String mimeType,String bucket, String objectName) {
            try {
                UploadObjectArgs testbucket = UploadObjectArgs.builder()
                        .bucket(bucket)
                        .object(objectName)
                        .filename(localFilePath)
                        .contentType(mimeType)
                        .build();
                minioClient.uploadObject(testbucket);
                log.debug("上传文件到minio成功,bucket:{},objectName:{}",bucket,objectName);
                System.out.println("上传成功");
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                log.error("上传文件到minio出错,bucket:{},objectName:{},错误原因:{}",bucket,objectName,e.getMessage(),e);
                XueChengPlusException.cast("上传文件到文件系统失败");
            }
            return false;
        }

    @Override
    public MediaFiles getFileById(String mediaId) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(mediaId);
        return mediaFiles;
    }

    /**
         * @description 将文件信息添加到media_files表
         * @param companyId  机构id
         * @param fileMd5  文件md5值
         * @param uploadFileParamsDto  上传文件的信息
         * @param bucket  桶
         * @param objectName 对象名称
         * @return com.xuecheng.media.model.po.MediaFiles
         * @author Mr.M
         * @date 2022/10/12 21:22
         */
        @Transactional
        public MediaFiles addMediaFilesToDb(Long companyId,String fileMd5,UploadFileParamsDto uploadFileParamsDto,String bucket,String objectName){
            //从数据库查询文件
            MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
            if (mediaFiles == null) {
                mediaFiles = new MediaFiles();
                //拷贝基本信息
                BeanUtils.copyProperties(uploadFileParamsDto, mediaFiles);
                mediaFiles.setId(fileMd5);
                mediaFiles.setFileId(fileMd5);
                mediaFiles.setCompanyId(companyId);
                mediaFiles.setUrl("/" + bucket + "/" + objectName);
                mediaFiles.setBucket(bucket);
                mediaFiles.setFilePath(objectName);
                mediaFiles.setCreateDate(LocalDateTime.now());
                mediaFiles.setAuditStatus("002003");
                mediaFiles.setStatus("1");
                //保存文件信息到文件表
                int insert = mediaFilesMapper.insert(mediaFiles);
                if (insert < 0) {
                    log.error("保存文件信息到数据库失败,{}",mediaFiles.toString());
                    XueChengPlusException.cast("保存文件信息失败");
                    return null;
                }
                log.debug("保存文件信息到数据库成功,{}",mediaFiles.toString());

            }
            //记录待处理任务
            //如果是avi视频写入待处理表
            addWaitingTask(mediaFiles);



            return mediaFiles;

        }

    /**
     * 添加等待处理的分布式任务
     * @param mediaFiles
     */
    public  void addWaitingTask(MediaFiles mediaFiles){

            //文件名称
            String filename = mediaFiles.getFilename();
            //文件拓展名
            String extension = filename.substring(filename.lastIndexOf("."));
            //获取文件的mineType
            String mimeType = getMimeType(extension);
            if (mimeType.equals("video/x-msvideo")){//如果是avi视频写入待处理任务
                MediaProcess mediaProcess = new MediaProcess();
                BeanUtils.copyProperties(mediaFiles,mediaProcess);
                mediaProcess.setStatus("1");  //1:未处理
                mediaProcess.setCreateDate(LocalDateTime.now());
                mediaProcess.setFailCount(0);//失败次数
                mediaProcess.setUrl(null);
                mediaProcessMapper.insert(mediaProcess);
            }
        }

        /**
         * 检测文件是否存在
         * @param fileMd5
         * @return
         */
        @Override
        public RestResponse<Boolean> checkFile(String fileMd5) {
            //先查询数据库
            MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
            if (mediaFiles!=null){
                String bucket = mediaFiles.getBucket();
                String filePath = mediaFiles.getFilePath();
                GetObjectArgs getObjectArgs = GetObjectArgs
                        .builder().bucket(bucket)
                        .object(filePath)
                        .build();
                //查询远程服务获得一个流对象
                try {
                    FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
                    //minio文件已经存在，不需要在传了
                    if (inputStream!=null){
                        return RestResponse.success(true);
                    }
                    inputStream.close();
                }catch (Exception e){
                }
            }
            //文件不存在
           return RestResponse.success(false);
        }
        /**
         * 检测分块信息是否完整
         * @param fileMd5
         * @param chunkIndex
         * @return
         */
        @Override
        public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {
            //根据md5得到分块文件所在的目录的路径
            String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
            //如果存在在查询minio
            GetObjectArgs getObjectArgs = GetObjectArgs
                    .builder().bucket(bucket_video)
                    .object(chunkFileFolderPath+chunkIndex)
                    .build();
            //查询远程服务获得一个流对象
            try {
                FilterInputStream inputStream = minioClient.getObject(getObjectArgs);
                //minio分块已经存在了
                if (inputStream!=null){
                    return RestResponse.success(true);
                }
                inputStream.close();
            }catch (Exception e){
            }
          //该分块不存在
            return RestResponse.success(false);
        }

        /**
         * 上传分块
         * @param fileMd5
         * @param chunkIndex
         * @param localChunkPath
         * @return
         */
        @Override
        public RestResponse<Boolean> uploadChunk(String fileMd5, int chunkIndex, String localChunkPath) {
            //将分块文件上传到minio
            //分块的路径
            String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunkIndex;
            //获取miniType
            String mimeType = getMimeType(null);
            boolean b = addMediaFilesToMinIO(localChunkPath, mimeType, bucket_video, chunkFilePath);
            //上传失败
            if (!b){
            return RestResponse.success(false,"上传分块出错");
            }
            //记录待处理任务

            return RestResponse.success(true);
        }

    /**
     * 合并分块
     * @param companyId
     * @param fileMd5
     * @param chunkTotal
     * @param uploadFileParamsDto
     * @return
     */
        @Override
        public RestResponse mergeChunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {
            //获取分块所在目录
            String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
            //指定分块文件的信息
            List<ComposeSource> sources = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> ComposeSource.builder()
                            .bucket(bucket_video)
                            .object(chunkFileFolderPath+i)
                            .build())
                    .collect(Collectors.toList());
            //源文件名称
            String filename = uploadFileParamsDto.getFilename();
            //拓展名
            String extension = filename.substring(filename.lastIndexOf("."));
            //合并后的文件名
            String objectName = getFilePathByMd5(fileMd5, extension);
            //对分块文件的信息进行合并
            ComposeObjectArgs composeObjectArgs = ComposeObjectArgs.builder().bucket(bucket_video).object(objectName).sources(sources).build();
            //合并文件
            try {
                minioClient.composeObject(composeObjectArgs);
            }catch (Exception e){
                e.printStackTrace();
                log.error("合并文件出错，bucket:{},object:{},错误信息:{}",bucket_video,objectName,e.getMessage());
                return RestResponse.validfail(false,"合并文件出错");
            }
            //校验合并后的文件md5是否一致
            //先下载合后的文件
            File file = downloadFileFromMinIO(bucket_video, objectName);
            //合并后文件的md5
            try  (FileInputStream fileInputStream = new FileInputStream(file);){
                String mergeFile_md5 = DigestUtils.md5Hex(fileInputStream);
                fileInputStream.close();
                //比较原始md5和合并后md5
                if (!fileMd5.equals(mergeFile_md5)){
                    log.error("校验合并文件md5值不一致,原始文件:{},合并后文件:{}",fileMd5,mergeFile_md5);
                    return RestResponse.validfail(false,"文件校验失败");
                }
                //设置文件大小
                uploadFileParamsDto.setFileSize(file.length());
            }catch (Exception e){
                return RestResponse.validfail(false,"文件校验失败");
            }
          //合并成功文件入库
            MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_video, objectName);
            if (mediaFiles==null){
                return RestResponse.validfail(false,"文件入库失败");
            }
            //清理分块
            clearChunk(chunkFileFolderPath,chunkTotal);

            return RestResponse.success(true);
        }

    /**
     * 上传完成清理分块
     * @param chunkFileFolderPath
     * @param chunkTotal
     */
        private void clearChunk(String chunkFileFolderPath, int chunkTotal) {
            List<DeleteObject> objects = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> new DeleteObject(chunkFileFolderPath + i))
                    .collect(Collectors.toList());
            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(bucket_video).objects(objects).build();
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            //真正删除
                results.forEach(f -> {
                    try {
                    DeleteError deleteError = f.get();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
        }
       //得到分块文件的目录
        private String getChunkFileFolderPath(String fileMd5) {
            return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
        }
        //得到合并后文件的地址
        private String getFilePathByMd5(String fileMd5,String fileExt) {
            return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5+fileExt;
        }
        /**
         * 从minio下载文件
         * @param bucket 桶
         * @param objectName 文件在桶的位置
         * @return 下载后的文件
         */
        public File downloadFileFromMinIO(String bucket,String objectName){
            //临时文件
            File minioFile = null;
            FileOutputStream outputStream = null;
            try{
                InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                        .bucket(bucket)
                        .object(objectName)
                        .build());
                //创建临时文件
                minioFile=File.createTempFile("minio", ".merge");
                outputStream = new FileOutputStream(minioFile);
                IOUtils.copy(stream,outputStream);
                return minioFile;
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                if(outputStream!=null){
                    try {
                        outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
            return null;
        }



        /**
         * 普通文件上传
         * @param companyId
         * @param uploadFileParamsDto
         * @param localFilePath
         * @return
         */
        @Override
        public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto uploadFileParamsDto, String localFilePath,String objectName) {
            File file = new File(localFilePath);
            if (!file.exists()) {
                XueChengPlusException.cast("文件不存在");
            }
            //文件名称
            String filename = uploadFileParamsDto.getFilename();
            //文件扩展名
            String extension = filename.substring(filename.lastIndexOf("."));
            //文件mimeType
            String mimeType = getMimeType(extension);
            //文件的md5值
            String fileMd5 = getFileMd5(file);
            //文件的默认目录
            String defaultFolderPath = getDefaultFolderPath();
            //存储到minio中的对象名(带目录)
            if (StringUtils.isEmpty(objectName)){
                //使用年月日上传
                objectName = defaultFolderPath + fileMd5 + extension;
            }
            //将文件上传到minio
            addMediaFilesToMinIO(localFilePath, mimeType, bucket_mediaFiles, objectName);
            //文件大小
            uploadFileParamsDto.setFileSize(file.length());
            //将文件信息存储到数据库
    //        MediaFiles mediaFiles = addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediaFiles, objectName);
            //使用生成的代理对象调用addMediaFilesToDb 使其被事务控制
            MediaFiles mediaFiles = currentProxy.addMediaFilesToDb(companyId, fileMd5, uploadFileParamsDto, bucket_mediaFiles, objectName);
            //准备返回数据
            UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
            BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
            return uploadFileResultDto;
        }

}
